/*
 * User: user
 * Date: 10/12/2007
 * Time: 12:05:09
 */
package javax.webbeans;

import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

/**
 * Represents a generic type {@code T}. Java doesn't yet provide a way to
 * represent generic types, so this class does. Forces clients to create a
 * subclass of this class which enables retrieval the type information even at
 * runtime.
 *
 * <p>For example, to create a type literal for {@code List<String>}, you can
 * create an empty anonymous inner class:
 *
 * <p>
 * {@code TypeLiteral<List<String>> list = new TypeLiteral<List<String>>() {};}
 *
 * <p>Assumes that type {@code T} implements {@link Object#equals} and
 * {@link Object#hashCode()} as value (as opposed to identity) comparison.
 *
 * @author crazybob@google.com (Bob Lee)
 */
public abstract class TypeLiteral<T> {

    private final Class<? super T> rawType;
    private final Type type;
    private final int hashCode;

    /**
     Constructs a new type literal. Derives represented class from type
     parameter.
     <p/>
     <p>Clients create an empty anonymous subclass. Doing so embeds the type
     parameter in the anonymous class's type hierarchy so we can reconstitute it
     at runtime despite erasure.
     */
    @SuppressWarnings("unchecked")
    protected TypeLiteral() {
        type = getSuperclassTypeParameter(getClass());
        rawType = (Class<? super T>) getRawType(type);
        hashCode = hashCode(type);
    }

    /** Unsafe. Constructs a type literal manually. */
    @SuppressWarnings("unchecked")
    protected TypeLiteral(Type type) {
        if (type == null) {
            throw new RuntimeException("Type cannot be null");
        }
        rawType = (Class<? super T>) getRawType(type);
        this.type = type;
        hashCode = hashCode(type);
    }

    /** Gets type from super class's type parameter. */
    static Type getSuperclassTypeParameter(Class<?> subclass) {
        Type superclass = subclass.getGenericSuperclass();
        if (superclass instanceof Class) {
            throw new RuntimeException("Missing type parameter.");
        }
        return ((ParameterizedType) superclass).getActualTypeArguments()[0];
    }

    /** Gets type literal from super class's type parameter. */
    static TypeLiteral<?> fromSuperclassTypeParameter(Class<?> subclass) {
        return new SimpleTypeLiteral<Object>(getSuperclassTypeParameter(subclass));
    }

    @SuppressWarnings({"unchecked"})
    private static Class<?> getRawType(Type type) {
        if (type instanceof Class<?>) {
            // type is a normal class.
            return (Class<?>) type;
        } else {
            if (type instanceof ParameterizedType) {
                ParameterizedType parameterizedType = (ParameterizedType) type;

                // I'm not exactly sure why getRawType() returns Type instead of Class.
                // Neal isn't either but suspects some pathological case related
                // to nested classes exists.
                Type rawType = parameterizedType.getRawType();
                if (!(rawType instanceof Class<?>)) {
                    throw unexpectedType(rawType, Class.class);
                }
                return (Class<?>) rawType;
            }

            if (type instanceof GenericArrayType) {
                // TODO: Is this sufficient?
                return Object[].class;
            }

            // type is a parameterized type.
            throw unexpectedType(type, ParameterizedType.class);
        }
    }

    /** Gets the raw type. */
    Class<? super T> getRawType() {
        return rawType;
    }

    /** Gets underlying {@code Type} instance. */
    public Type getType() {
        return type;
    }

    @Override
    public int hashCode() {
        return hashCode;
    }

    @SuppressWarnings({"AccessingNonPublicFieldOfAnotherObject"})
    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }
        if (!(o instanceof TypeLiteral<?>)) {
            return false;
        }
        TypeLiteral<?> other = (TypeLiteral<?>) o;

        return equals(type, other.type);
    }

    @Override
    public String toString() {
        return type instanceof Class<?> ? ((Class<?>) type).getName() : type.toString();
    }

    static AssertionError unexpectedType(Type type, Class<?> expected) {
        return new AssertionError("Unexpected type. Expected: " + expected.getName() + ", got: " + type.getClass().getName() + ", for type literal: " + type.toString() + ".");
    }

    /** Gets type literal for the given {@code Type} instance. */
    public static TypeLiteral<?> get(Type type) {
        return new SimpleTypeLiteral<Object>(type);
    }

    /** Gets type literal for the given {@code Class} instance. */
    public static <T> TypeLiteral<T> get(Class<T> type) {
        return new SimpleTypeLiteral<T>(type);
    }

    private static class SimpleTypeLiteral<T> extends TypeLiteral<T> {
        SimpleTypeLiteral(Type type) {
            super(type);
        }
    }

    static int hashCode(Type type) {
        if (type instanceof ParameterizedType) {
            ParameterizedType p = (ParameterizedType) type;
            int h = p.getRawType().hashCode();
            for (Type argument : p.getActualTypeArguments()) {
                h = h * 31 + hashCode(argument);
            }
            return h;
        }

        if (type instanceof Class) {
            // Class specifies hashCode().
            return type.hashCode();
        }

        if (type instanceof GenericArrayType) {
            return hashCode(((GenericArrayType) type).getGenericComponentType()) * 31;
        }

        // This isn't a type we support. Could be a generic array type, wildcard
        // type, etc.
        return type.hashCode();
    }

    static boolean equals(Type a, Type b) {
        if (a instanceof Class) {
            // Class already specifies equals().
            return a.equals(b);
        }

        if (a instanceof ParameterizedType) {
            if (!(b instanceof ParameterizedType)) {
                return false;
            }

            ParameterizedType pa = (ParameterizedType) a;
            ParameterizedType pb = (ParameterizedType) b;

            if (!pa.getRawType().equals(pb.getRawType())) {
                return false;
            }

            Type[] aa = pa.getActualTypeArguments();
            Type[] ba = pb.getActualTypeArguments();
            if (aa.length != ba.length) {
                return false;
            }

            for (int i = 0; i < aa.length; i++) {
                if (!equals(aa[i], ba[i])) {
                    return false;
                }
            }

            return true;
        }

        if (a instanceof GenericArrayType) {
            if (!(b instanceof GenericArrayType)) {
                return false;
            }

            return equals(((GenericArrayType) a).getGenericComponentType(), ((GenericArrayType) b).getGenericComponentType());
        }

        // This isn't a type we support. Could be a generic array type, wildcard
        // type, etc.
        return false;
    }
}